/*
 * Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies).
 * All rights reserved.
 *
 * This file is part of Harmattan SmashMiner OpenGL game application.
 * 
 * Harmattan SmashMiner OpenGL Example Application version 1.0.0
 *  
 * Latest update: 15.4.2011
 *
 * The Harmattan SmashMiner OpenGL example application demonstrates how to use 
 * the OpenGL ES in Harmattan devices.
 *
 * This example is provided as a starting point for 3rd party
 * developers (you) to ease the implementation of OpenGL based 
 * games. Ideas, parts of code or methodologies that this
 * example application uses can be freely used without restrictions.
 *
 * See file README.txt how to build and compile this example application
 * in the Harmattan SDK environment. 
 * 
 * See file INSTALL.txt to find out what is required to run this
 * application and how to install the application to the device or
 * alternatively in QEMU-emulator.
 *
 * The LICENSE.txt file explains the license this software is provided
 * with. A copy of the same license is included in this source file.
 * 
 */

/*
 * Copyright (C) 2011 by Nokia Corporation.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "gamewidget.h"

#ifndef SMASHMINER_CONFIG_USE_OPENGL_ES_2

#include "spacecraft.h"
#include "blackhole.h"
#include "meteorshower.h"
#include "asteroid.h"
#include "comet.h"
#include "ufo.h"
#include "gameengine.h"
#include "timermanager.h"
#include "collectible.h"
#include "hyperjumpnavigator.h"
#include "visualeffectdata.h"
#include <math.h>

#include <QDebug>

void GameWidget::GLInitialize()
{
    GLSetupViewPort();

    // set some opengl initial settings.
    //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

    glFrontFace(GL_CW);     // define clockwise-wound polygons as front-facing.

    GLfloat glob_ambient[] = { 0.1f, 0.1f, 0.1f, 1.0f };
    GLfloat light_ambient[] = { 0.3f, 0.3f, 0.3f, 1.0f };

    GLfloat light_diffuse[] = { 0.5f, 0.5f, 0.5f, 1.0f };

    GLfloat light_pos[] = { +0.4f, +0.6f, -1.0f, 0.0f };
    GameEngine::normalizeVect3D_GLfloat(light_pos);

    glLightModelfv(GL_LIGHT_MODEL_AMBIENT, glob_ambient);

    glLightfv(GL_LIGHT0, GL_AMBIENT, light_ambient);
    glLightfv(GL_LIGHT0, GL_DIFFUSE, light_diffuse);
    glLightfv(GL_LIGHT0, GL_POSITION, light_pos);
    glEnable(GL_LIGHT0);

    glDepthFunc(GL_LESS);
    glEnable(GL_DEPTH_TEST);

    glEnable(GL_COLOR_MATERIAL);
    glColorMaterial(GL_FRONT, GL_AMBIENT_AND_DIFFUSE);

    GLInitBackGround();
    initObjects();
}

void GameWidget::GLSetupViewPort()
{
    setViewRanges();

    glViewport(0, 0, engine->gameScreenWidth, engine->gameScreenHeight);

    // use an orthographic projection since it is faster
    // and we don't really need perspective in a 2D game.

    glMatrixMode(GL_PROJECTION);

    glLoadIdentity();
    glOrtho(-engine->gameViewRangeX, +engine->gameViewRangeX,
            -engine->gameViewRangeY, +engine->gameViewRangeY,
            -engine->gameViewRangeZ, +engine->gameViewRangeZ);

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
}

void GameWidget::GLInitBackGround()
{
    const GLfloat grid_x_step = 2.0 * engine->gameViewRangeX / (GLfloat) BG_GRID_X_SIZE;
    const GLfloat grid_y_step = 2.0 * engine->gameViewRangeY / (GLfloat) BG_GRID_Y_SIZE;

    GLfloat posX;
    GLfloat posY;

    backgroundData.resize(0);

    // add vertical lines

    posX = -engine->gameViewRangeX;
    for (int x = 0;x <= BG_GRID_X_SIZE;x++) {
        posY = -engine->gameViewRangeY;
        for (int y = 0;y <= BG_GRID_Y_SIZE;y++) {
            backgroundData.push_back(posX);
            backgroundData.push_back(posY);
            backgroundData.push_back(-40.0);

            backgroundData.push_back(posX);
            backgroundData.push_back(posY + grid_y_step);
            backgroundData.push_back(-40.0);

            posY += grid_y_step;
        }

        posX += grid_x_step;
    }

    // add horizontal lines

    posY = -engine->gameViewRangeY;
    for (int y = 0;y <= BG_GRID_Y_SIZE;y++) {
        posX = -engine->gameViewRangeX;
        for (int x = 0;x <= BG_GRID_X_SIZE;x++) {
            backgroundData.push_back(posX);
            backgroundData.push_back(posY);
            backgroundData.push_back(-40.0);

            backgroundData.push_back(posX + grid_x_step);
            backgroundData.push_back(posY);
            backgroundData.push_back(-40.0);

            posX += grid_x_step;
        }

        posY += grid_y_step;
    }

    // ready...
}

void GameWidget::GLPaint(GLfloat timestep)
{
    paintWithBlackHole = (engine->blackhole != NULL && engine->blackhole->isAlive());

    GLPaintBackGround();

    glEnable(GL_LIGHTING);

    // paint the game objects.
    //^^^^^^^^^^^^^^^^^^^^^^^^^
    // if normal game mode -> paint using current location.
    // if rewind mode -> paint using a stored location.

    bool visible;
    GLfloat pos[2];

    // paint the spacecraft (also mirrored if needed).

    visible = isObjectVisible(engine->miner);

    if (engine->timerManager->isActive(GameTimer::stateGetReadyTimer))
        visible = false;

    if (engine->timerManager->hyperJumpOutData != NULL && engine->timerManager->hyperJumpOutData->isHidden())
        visible = false;

    if (engine->timerManager->hyperJumpInData != NULL && engine->timerManager->hyperJumpInData->isHidden())
        visible = false;

    if (visible) {
        getObjectPosition(engine->miner, pos);
        GLPaintSpaceShip(pos[0], pos[1]);

        GLPaintObjectRadius(engine->miner);

#ifdef SMASHMINER_CONFIG_ENABLE_GAME_REWIND_MODE
        if (rewindMode)
            engine->miner->checkWrappingRewind(pos, engine->gameViewRangeX, engine->gameViewRangeY);
#endif  // SMASHMINER_CONFIG_ENABLE_GAME_REWIND_MODE

        const bool wrapX = (engine->miner->wrappingFlags() & WRAPPING_FLAG_WRAP_X);
        if (wrapX)
            GLPaintSpaceShip(engine->miner->wrappedPosX(), pos[1]);

        const bool wrapY = (engine->miner->wrappingFlags() & WRAPPING_FLAG_WRAP_Y);
        if (wrapY)
            GLPaintSpaceShip(pos[0], engine->miner->wrappedPosY());

        if (wrapX && wrapY)
            GLPaintSpaceShip(engine->miner->wrappedPosX(), engine->miner->wrappedPosY());
    }

    // paint the asteroids (also mirrored if needed).

    for (int i = 0;i < engine->asteroidList.count();i++) {
        Asteroid *a = engine->asteroidList[i];

        visible = isObjectVisible(a);

        if (visible) {
            getObjectPosition(a, pos);
            GLPaintAsteroid(pos[0], pos[1], a);

            GLPaintObjectRadius(a);

#ifdef SMASHMINER_CONFIG_ENABLE_GAME_REWIND_MODE
            if (rewindMode)
                a->checkWrappingRewind(pos, engine->gameViewRangeX, engine->gameViewRangeY);
#endif  // SMASHMINER_CONFIG_ENABLE_GAME_REWIND_MODE

            const bool wrapX = (a->wrappingFlags() & WRAPPING_FLAG_WRAP_X);
            if (wrapX)
                GLPaintAsteroid(a->wrappedPosX(), pos[1], a);

            const bool wrapY = (a->wrappingFlags() & WRAPPING_FLAG_WRAP_Y);
            if (wrapY)
                GLPaintAsteroid(pos[0], a->wrappedPosY(), a);

            if (wrapX && wrapY)
                GLPaintAsteroid(a->wrappedPosX(), a->wrappedPosY(), a);
        }
    }

    for (int i = 0; i < engine->collectibleList.count(); i++) {
        Collectible *c = engine->collectibleList[i];

        visible = isObjectVisible(c);

        if (visible) {
            getObjectPosition(c, pos);
            GLPaintCollectible(pos[0], pos[1], c);

            GLPaintObjectRadius(c);

#ifdef SMASHMINER_CONFIG_ENABLE_GAME_REWIND_MODE
            if (rewindMode)
                c->checkWrappingRewind(pos, engine->gameViewRangeX, engine->gameViewRangeY);
#endif  // SMASHMINER_CONFIG_ENABLE_GAME_REWIND_MODE

            const bool wrapX = (c->wrappingFlags() & WRAPPING_FLAG_WRAP_X);
            if (wrapX)
                GLPaintCollectible(c->wrappedPosX(), pos[1], c);

            const bool wrapY = (c->wrappingFlags() & WRAPPING_FLAG_WRAP_Y);
            if (wrapY)
                GLPaintCollectible(pos[0], c->wrappedPosY(), c);

            if (wrapX && wrapY)
                GLPaintCollectible(c->wrappedPosX(), c->wrappedPosY(), c);

        }
    }

    for (int i = 0;i < engine->enemyList.size();i++) {
        GameObject *go = engine->enemyList[i];

        visible = isObjectVisible(go);

        if (visible) {
            GLPaintObjectRadius(go);

            getObjectPosition(go, pos);

            if (go->objectType() == TypeUfo) {
                Ufo *u = static_cast<Ufo *>(go);
                GLPaintUfo(pos[0], pos[1], u);
            }

            if (go->objectType() == TypeMeteorShower) {
                MeteorShower *ms = static_cast<MeteorShower *>(go);

                GLfloat currentTime;
    #ifdef SMASHMINER_CONFIG_ENABLE_GAME_REWIND_MODE
                if (rewindMode)
                    currentTime = (*rewindCurrentFrameInfo)->timeStamp();
                else
                    currentTime = engine->totalTimeCounter;
    #else   // SMASHMINER_CONFIG_ENABLE_GAME_REWIND_MODE
                currentTime = engine->totalTimeCounter;
    #endif  // SMASHMINER_CONFIG_ENABLE_GAME_REWIND_MODE

                GLPaintMeteorShower(pos[0], pos[1], ms, currentTime);
            }
        }
    }

    if (engine->comet != NULL) {
        visible = isObjectVisible((GameObject *) engine->comet);
        if (visible) {
            getObjectPosition((GameObject *) engine->comet, pos);

            GLfloat currentTime;
#ifdef SMASHMINER_CONFIG_ENABLE_GAME_REWIND_MODE
            if (rewindMode)
                currentTime = (*rewindCurrentFrameInfo)->timeStamp();
            else
                currentTime = engine->totalTimeCounter;
#else   // SMASHMINER_CONFIG_ENABLE_GAME_REWIND_MODE
            currentTime = engine->totalTimeCounter;
#endif  // SMASHMINER_CONFIG_ENABLE_GAME_REWIND_MODE

            GLPaintComet(pos[0], pos[1], engine->comet, currentTime);
        }
    }

    // visualize HyperJumpNavigator iteration(s) if it exists.
    //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

    if (engine->hyperJumpNavigator) {
        GameObject tmpGO(TypeCollectibleHyperJump, engine->totalFrameCounter,
                         2.5f, SpeedLimitLow, engine->rewindEnabled, MassTiny, ScoreNone);
        for (int i = 0;i < engine->hyperJumpNavigator->destList.count();i++) {
            HyperJumpDestination * dest = engine->hyperJumpNavigator->destList.at(i);

            tmpGO.setPosX(dest->posX);
            tmpGO.setPosY(dest->posY);

            GLPaintObjectRadius(&tmpGO);
        }
    }

#ifdef SMASHMINER_CONFIG_ENABLE_DEBUG_CHECKS

    // visualize the stored GameObject start-parameters.
    //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

    for (int i = 0;i < engine->storedStartParamsDEBUG.count();i++) {
        GameObjectStartParameters sp = engine->storedStartParamsDEBUG.at(i);

        const GLfloat startX = sp.startX;
        const GLfloat startY = sp.startY;

        GLfloat endX;
        GLfloat endY;

        glBegin(GL_LINES);

        // white-blue line = the original endpoint.

        glColor3f(1.0f, 1.0f, 1.0f);
        glVertex3f(startX, startY, -50.0f);

        glColor3f(0.0f, 0.0f, 1.0f);
        glVertex3f(sp.endX, sp.endY, -50.0f);

        // white-red line = angle-based endpoint.

        glColor3f(1.0f, 1.0f, 1.0f);
        glVertex3f(startX, startY, -50.0f);

        endX = startX + sp.unitVelX * 250.0f;
        endY = startY + sp.unitVelY * 250.0f;

        glColor3f(0.0f, 1.0f, 0.0f);
        glVertex3f(endX, endY, -50.0f);

        // white-red line = angle-based endpoint.

        glColor3f(1.0f, 1.0f, 1.0f);
        glVertex3f(startX, startY, -50.0f);

        endX = startX + sin(sp.angle) * 250.0f;
        endY = startY + cos(sp.angle) * 250.0f;

        glColor3f(1.0f, 0.0f, 0.0f);
        glVertex3f(endX, endY, -50.0f);

        glEnd();
    }

#endif  // SMASHMINER_CONFIG_ENABLE_DEBUG_CHECKS

    // do next painting operations...
    // <pistä seuraavat piirtokomennot tähän>

    glDisable(GL_LIGHTING);

    // paint the particles.

    GLPaintParticles(timestep);

    // paint the control gauges if needed.

#ifdef SMASHMINER_CONFIG_ENABLE_CONTROL_GAUGES
#ifndef SMASHMINER_CONFIG_ENABLE_GAME_REWIND_MODE
    GLPaintControlGauges();
#else   // SMASHMINER_CONFIG_ENABLE_GAME_REWIND_MODE
    if (rewindMode)
        GLPaintRewindGauge();
    else
        GLPaintControlGauges();
#endif  // SMASHMINER_CONFIG_ENABLE_GAME_REWIND_MODE
#endif  // SMASHMINER_CONFIG_ENABLE_CONTROL_GAUGES

    // ready...
}

void GameWidget::GLPaintBackGround()
{
    // Two optional ways to draw backoground. Currently option 1 is used.
    //^^^^^^^^^
    // option 1 : paint the entire screen area using a texture AND skip glClear(GL_COLOR_BUFFER_BIT)
    // option 2 : do first glClear(GL_COLOR_BUFFER_BIT) with black and then paint some background textures.

    glBegin(GL_LINES);
    glColor3f(0.0f, 0.0f, 1.0f);

    const int sz = backgroundData.size() / 6;
    for (int i = 0;i < sz;i++) {
        glVertex3fv(&backgroundData[i * 6 + 0]);
        glVertex3fv(&backgroundData[i * 6 + 3]);
    }

    glEnd();
}

void GameWidget::GLPaintSpaceShip(GLfloat pos_x, GLfloat pos_y)
{
    glPushMatrix();
    glTranslatef(pos_x, pos_y, 0.0);

// not needed here, but proves that triangle facing is OK.
//glEnable(GL_CULL_FACE);

    // draw the SpaceShip using the traditional way (a debug option):
    //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

/*  glColor4fv(cone_color);
    GL_drawTriangles(spaceship_cone_vertex_data, spaceship_cone_normal_data, SPACESHIP_CONE_INDICES / 9);

    glColor4fv(ball_color);
    GL_drawTriangles(spaceship_ball_vertex_data, spaceship_ball_normal_data, SPACESHIP_BALL_INDICES / 9);   */

    // draw the SpaceShip using arrays:
    //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_NORMAL_ARRAY);

    glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, spaceshipConeColorData);
    glVertexPointer(3, GL_FLOAT, 0, spaceshipConeVertexData);
    glNormalPointer(GL_FLOAT, 0, spaceshipConeNormalData);
    glDrawArrays(GL_TRIANGLES, 0, SPACESHIP_CONE_INDICES / 3);

    glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, spaceshipBallColorData);
    glVertexPointer(3, GL_FLOAT, 0, spaceshipBallVertexData);
    glNormalPointer(GL_FLOAT, 0, spaceshipBallNormalData);
    glDrawArrays(GL_TRIANGLES, 0, SPACESHIP_BALL_INDICES / 3);

    glDisableClientState(GL_VERTEX_ARRAY);
    glDisableClientState(GL_NORMAL_ARRAY);

// not needed here, but proves that triangle facing is OK.
//glDisable(GL_CULL_FACE);

    glPopMatrix();
}

void GameWidget::GLPaintAsteroid(GLfloat pos_x, GLfloat pos_y, Asteroid * a)
{
    GLfloat *vertexData = vertexDataByType(a);
    GLfloat *normalData = normalDataByType(a);
    GLfloat *colourData = colourDataByType(a);

    int indCount;
    indexCountByType(a, indCount);

    glPushMatrix();
    glTranslatef(pos_x, pos_y, a->posZ());

    glRotatef(a->rotationAngle(), a->rotationAxis(0), a->rotationAxis(1), a->rotationAxis(2));

    glEnable(GL_CULL_FACE);

    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_NORMAL_ARRAY);
    glEnableClientState(GL_COLOR_ARRAY);

    glColor3f(0.5, 0.5, 1.0);
    glVertexPointer(3, GL_FLOAT, 0, vertexData);
    glNormalPointer(GL_FLOAT, 0, normalData);
    glColorPointer(3, GL_FLOAT, 0, colourData);
    glDrawArrays(GL_TRIANGLES, 0, indCount / 3);

    glDisableClientState(GL_VERTEX_ARRAY);
    glDisableClientState(GL_NORMAL_ARRAY);
    glDisableClientState(GL_COLOR_ARRAY);

    glDisable(GL_CULL_FACE);

    glPopMatrix();
}

void GameWidget::GLPaintCollectible(GLfloat posX, GLfloat posY, Collectible *col)
{
    switch (col->objectType()) {
        case TypeCollectibleBomb:       // fall through
        case TypeCollectibleHyperJump:
            GLPaintObjectRadius(col);
            break;
        case TypeCollectibleMineralA:   // fall through
        case TypeCollectibleMineralB:   // fall through
        case TypeCollectibleMineralC:
        case TypeCollectibleMineralD:
            GLPaintRotatingCollectible(posX, posY, static_cast<RotatingCollectible *>(col));
            break;
        default:
            qFatal("Type %d not supported collectible type.", col->objectType());
    }
}

void GameWidget::GLPaintRotatingCollectible(GLfloat posX, GLfloat posY, RotatingCollectible *col)
{
    GLfloat *vertexData = vertexDataByType(col);
    GLfloat *normalData = normalDataByType(col);
    GLfloat *colourData = colourDataByType(col);

    int indCount;
    indexCountByType(col, indCount);

    glPushMatrix();
    glTranslatef(posX, posY, 0.0);
    glEnable(GL_CULL_FACE);

    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_NORMAL_ARRAY);

    glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, colourData);
    glVertexPointer(3, GL_FLOAT, 0, vertexData);
    glNormalPointer(GL_FLOAT, 0, normalData);
    glDrawArrays(GL_TRIANGLES, 0, indCount / 3);
    
    glDisableClientState(GL_VERTEX_ARRAY);
    glDisableClientState(GL_NORMAL_ARRAY);
    
    glDisable(GL_CULL_FACE);
    glPopMatrix();
}

void GameWidget::GLPaintUfo(GLfloat pos_x, GLfloat pos_y, Ufo *ufo)
{
    GLfloat color1[] = { 1.5f, 0.5f, 0.0f, 1.0f };
    GLfloat color2[] = { 0.5f, 1.5f, 0.0f, 1.0f };
    GLfloat colorLight[] = { 1.0f, 1.0f, 1.0f, 1.0f };

    glPushMatrix();
    glTranslatef(pos_x, pos_y, 0.0);

    glEnable(GL_CULL_FACE);

    glEnableClientState (GL_VERTEX_ARRAY);
    glEnableClientState(GL_NORMAL_ARRAY);

    glRotatef(ufo->rotationAngle(), ufo->rotationAxis(0), ufo->rotationAxis(1), ufo->rotationAxis(2));

    glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, color1);
    glVertexPointer (3, GL_FLOAT, 0, ufoColor1VertexData);
    glNormalPointer(GL_FLOAT, 0, ufoColor1NormalData);
    glDrawArrays (GL_TRIANGLES, 0, UFO_COLOR1_INDICES/3);

    glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, color2);
    glVertexPointer (3, GL_FLOAT, 0, ufoColor2VertexData);
    glNormalPointer(GL_FLOAT, 0, ufoColor2NormalData);
    glDrawArrays (GL_TRIANGLES, 0, UFO_COLOR2_INDICES/3);

    glDisableClientState(GL_VERTEX_ARRAY);
    glDisableClientState(GL_NORMAL_ARRAY);

    glDisable(GL_CULL_FACE);

    glPopMatrix();

    glPushMatrix();
    glTranslatef(pos_x, pos_y, 0.0);

    glEnable(GL_CULL_FACE);

    glEnableClientState (GL_VERTEX_ARRAY);

    glRotatef(-ufo->rotationAngle(), ufo->rotationAxis(0), ufo->rotationAxis(1), ufo->rotationAxis(2));

    glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, colorLight);
    glVertexPointer (3, GL_FLOAT, 0, ufoLightsVertexData);
    glDrawArrays (GL_TRIANGLES, 0, UFO_LIGHTS_INDICES/3);

    glDisableClientState(GL_VERTEX_ARRAY);

    glDisable(GL_CULL_FACE);

    glPopMatrix();
}

void GameWidget::GLPaintComet(GLfloat posX, GLfloat posY, Comet * c, GLfloat time)
{
    glPushMatrix();
    glTranslatef(posX, posY, 0.0);

    GLfloat angle = 180.0f * c->rotationAngle() / M_PI;
    glRotatef(angle, c->rotationAxis(0), c->rotationAxis(1), c->rotationAxis(2));

    const GLfloat radius = c->objectRadius();

    const GLfloat length = c->getLength(time);
    const GLfloat width = c->getWidth(time);

    const GLfloat z = -25.0f;

    glColor3f(0.8f, 0.8f, 0.8f);

    glBegin(GL_QUADS);
    glVertex3f(-radius, +radius, z);
    glVertex3f(+radius, +radius, z);
    glVertex3f(+width, -length, z);
    glVertex3f(-width, -length, z);
    glEnd();

    glPopMatrix();
}

void GameWidget::GLPaintMeteorShower(GLfloat posX, GLfloat posY, MeteorShower *ms, GLfloat time)
{
    ms->animate(time);

    glPushMatrix();
    glTranslatef(posX, posY, 0.0);

    GLfloat *vertices = ms->vertexArray();
    GLfloat *colors = ms->colorArray();

    const int numVertices = ms->numVertices();

    glBegin(GL_TRIANGLES);

    for (int i = 0;i < numVertices;i++) {
        glColor3fv(&colors[i * 3]);
        glVertex3fv(&vertices[i * 3]);
    }

    glEnd();
    glPopMatrix();
}

void GameWidget::GLPaintParticles(GLfloat timestep)
{
    int readIndex;
    GLfloat currentTime;

    static GLfloat hyperJumpAngle = 0.0f;
    hyperJumpAngle += timestep * 0.010f;
    if (hyperJumpAngle > 2 * M_PI)
        hyperJumpAngle -= 2 * M_PI;

#ifdef SMASHMINER_CONFIG_ENABLE_GAME_REWIND_MODE
    if (rewindMode) {
        readIndex = (*rewindCurrentFrameInfo)->readIndexR();
        if (readIndex < 0)
            return;

        currentTime = (*rewindCurrentFrameInfo)->timeStamp();
    } else {
        readIndex = engine->particleData().particleWriteIndex - 1;
        if (readIndex < 0)
            readIndex = SMASHMINER_CONFIG_PARTICLE_BUFFER_CAPACITY - 1;

        currentTime = engine->totalTimeCounter;
    }
#else
    readIndex = engine->particleWriteIndex - 1;
        if (readIndex < 0)
            readIndex = SMASHMINER_CONFIG_PARTICLE_BUFFER_CAPACITY - 1;

        currentTime = engine->totalTimeCounter;
#endif

    const GLfloat timeLimit = currentTime - SMASHMINER_CONFIG_PARTICLE_LIFETIME;
    if (engine->particleData().particleBuffer[readIndex].startTime < timeLimit)
        return;     // nothing to paint...

    glPointSize(2.0);
    glBegin(GL_POINTS);

    // the while-loop below will cycle infinitely if ALL particles
    // in the particleBuffer have their ages below particle lifetime.
    // this may happen is the particleBuffer size is severely
    // underestimated (compared to particle lifetime).

    int infiniteLoopBreaker = 0;

    const GLfloat offScreen = engine->gameViewRangeX + engine->gameViewRangeY;

    GLfloat bhRad = 0.0f;
    GLfloat bhPos[2] = { offScreen, offScreen };
    if (paintWithBlackHole) {
        bhRad = engine->blackhole->objectRadius();
        getObjectPosition(engine->blackhole, bhPos);
    }

    while (engine->particleData().particleBuffer[readIndex].startTime > timeLimit) {
        const GLfloat ageAbs = currentTime - engine->particleData().particleBuffer[readIndex].startTime;
        const GLfloat ageRel = ageAbs / SMASHMINER_CONFIG_PARTICLE_LIFETIME;

        const GLfloat alpha = 1.0f - ageRel * ageRel;

        GLfloat tmpX;
        GLfloat tmpY;
        GLfloat tmpZ;

        if (engine->particleData().particleBuffer[readIndex].particleType == ParticleTypeBlackHole) {

            // handle a blackhole particle.
            //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
            // multiply tmpDist by alpha so that particles seem to "fall in".

            GLfloat tmpAngle = engine->particleData().pBufferB[readIndex].friction;

            const GLfloat tmpDist = engine->particleData().particleBuffer[readIndex].startPosX * alpha;
            const GLfloat tmpSin = sin(tmpAngle) * tmpDist;
            const GLfloat tmpCos = cos(tmpAngle) * tmpDist;

            tmpX = tmpSin * bhParticlesXAxis[0] + tmpCos * bhParticlesYAxis[0] + bhPos[0];
            tmpY = tmpSin * bhParticlesXAxis[1] + tmpCos * bhParticlesYAxis[1] + bhPos[1];
            tmpZ = tmpSin * bhParticlesXAxis[2] + tmpCos * bhParticlesYAxis[2];

            // update the particle radial motion.

            const GLfloat angleChange = engine->particleData().particleBuffer[readIndex].startPosY * timestep;

            tmpAngle += angleChange;
            if (tmpAngle > 2 * M_PI)
                tmpAngle -= 2 * M_PI;

            engine->particleData().pBufferB[readIndex].friction = tmpAngle;
        }
        else if (engine->particleData().particleBuffer[readIndex].particleType == ParticleTypeHyperJump) {

            // handle a HyperJump particle.
            //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
            // use a stored position and mode to compute the effect.

            const int mode = (engine->particleData().pBufferB[readIndex].friction < 0.50f ? 0 : 1);

            GLfloat tmpRelT;
            if (!mode) tmpRelT = engine->timerManager->timeRel(GameTimer::hyperJumpOutTimer);
            else tmpRelT = engine->timerManager->timeRel(GameTimer::hyperJumpInTimer);

            const GLfloat shape = sin(tmpRelT * 3.141592f);

            const GLfloat tmpAngle = hyperJumpAngle + engine->particleData().particleBuffer[readIndex].startFrict;
            const GLfloat tmpDist = shape * shape * 10.0f;
            const GLfloat tmpSin = sin(tmpAngle) * tmpDist;
            const GLfloat tmpCos = cos(tmpAngle) * tmpDist;

            const GLfloat tmpPosX = engine->particleData().particleBuffer[readIndex].startPosX;
            const GLfloat tmpPosY = engine->particleData().particleBuffer[readIndex].startPosY;

            tmpX = tmpSin * bhParticlesXAxis[0] + tmpCos * bhParticlesYAxis[0] + tmpPosX;
            tmpY = tmpSin * bhParticlesXAxis[1] + tmpCos * bhParticlesYAxis[1] + tmpPosY;
            tmpZ = 0.0;

        } else {

            // handle a normal particle.
            //^^^^^^^^^^^^^^^^^^^^^^^^^^^

            // update the particle position.

            tmpX = engine->particleData().particleBuffer[readIndex].velocityX * timestep;
            tmpY = engine->particleData().particleBuffer[readIndex].velocityY * timestep;

            engine->particleData().pBufferA[readIndex].positionX += tmpX;
            engine->particleData().pBufferA[readIndex].positionY += tmpY;

            // test if the particle dropped into a blackhole.
            // if it did, then stop it completely and move it into the
            // center of the blackhole -> it seems to disappear...

            if (paintWithBlackHole) {
                GLfloat distX = engine->particleData().pBufferA[readIndex].positionX - bhPos[0];
                GLfloat distY = engine->particleData().pBufferA[readIndex].positionY - bhPos[1];
                if (fabs(distX) < bhRad && fabs(distY) < bhRad) {
                    engine->particleData().pBufferA[readIndex].positionX = bhPos[0];
                    engine->particleData().pBufferA[readIndex].positionY = bhPos[1];
                    engine->particleData().particleBuffer[readIndex].velocityX = 0.0;
                    engine->particleData().particleBuffer[readIndex].velocityY = 0.0;
                    engine->particleData().pBufferB[readIndex].friction = 0.0;
                }
            }

            // update the particle velocity.

            GLfloat f = engine->particleData().pBufferB[readIndex].friction * timestep;

            tmpX = engine->particleData().particleBuffer[readIndex].startVelX * f;
            tmpY = engine->particleData().particleBuffer[readIndex].startVelY * f;

            engine->particleData().particleBuffer[readIndex].velocityX -= tmpX;
            engine->particleData().particleBuffer[readIndex].velocityY -= tmpY;

            // read the particle position for painting.

            tmpX = engine->particleData().pBufferA[readIndex].positionX;
            tmpY = engine->particleData().pBufferA[readIndex].positionY;
            tmpZ = engine->particleData().pBufferA[readIndex].positionZ;
        }

        // paint the particle.

        if (!rewindMode)
            engine->currentFrameStats.numParticles++;

        glColor3f(alpha, alpha, alpha);
        glVertex3f(tmpX, tmpY, tmpZ);

        if (infiniteLoopBreaker++ >= SMASHMINER_CONFIG_PARTICLE_BUFFER_CAPACITY)
            break;

        if (--readIndex < 0)
            readIndex = SMASHMINER_CONFIG_PARTICLE_BUFFER_CAPACITY - 1;
    }

    glEnd();
}

void GameWidget::GLPaintObjectRadius(GameObject * go)
{
    glPushMatrix();
    glTranslatef(go->posX(), go->posY(), 0.0);

    GLfloat angle;
    const GLfloat r = go->objectRadius();
    const GLfloat z = -35.0f;

    glColor3f(0.0f, 1.0f, 0.0f);

    glBegin(GL_TRIANGLES);

    for (GLfloat x = 0.0;x < 355.0;x += 30.0) {
        glVertex3f(0.0f, 0.0f, z);

        angle = x * M_PI / 180.0;
        glVertex3f(sin(angle) * r, cos(angle) * r, z);

        angle += 30.0 * M_PI / 180.0;
        glVertex3f(sin(angle) * r, cos(angle) * r, z);
    }
    glEnd();

    glPopMatrix();
}

#ifdef SMASHMINER_CONFIG_ENABLE_CONTROL_GAUGES

void GameWidget::GLPaintControlGauges()
{
    const GLfloat gauge_x_pos = 0.95 * engine->gameViewRangeX;
    const GLfloat gauge_y_pos = 0.95 * engine->gameViewRangeY;

    const GLfloat gauge_x_range = 0.90 * engine->gameViewRangeX;
    const GLfloat gauge_y_range = 0.90 * engine->gameViewRangeY;

    const GLfloat gauge_x_value = gauge_x_range * engine->steeringX();
    const GLfloat gauge_y_value = gauge_y_range * engine->steeringY();

    const GLfloat gauge_x_thickness = 0.01 * engine->gameViewRangeX;
    const GLfloat gauge_y_thickness = 0.01 * engine->gameViewRangeY;

    const GLfloat z = +75.0f;

    glColor3f(0.0f, 1.0f, 0.0f);

    glBegin(GL_LINES);

    glVertex3f(-gauge_x_range, -gauge_y_pos, z);    // x-gauge
    glVertex3f(+gauge_x_range, -gauge_y_pos, z);

    glVertex3f(-gauge_x_pos, -gauge_y_range, z);    // y-gauge
    glVertex3f(-gauge_x_pos, +gauge_y_range, z);

    glEnd();

    glBegin(GL_QUADS);

    glVertex3f(0.0, -gauge_y_pos - gauge_x_thickness, z);   // x-gauge
    glVertex3f(0.0, -gauge_y_pos + gauge_x_thickness, z);
    glVertex3f(gauge_x_value, -gauge_y_pos + gauge_x_thickness, z);
    glVertex3f(gauge_x_value, -gauge_y_pos - gauge_x_thickness, z);

    glVertex3f(-gauge_x_pos - gauge_y_thickness, 0.0, z);   // y-gauge
    glVertex3f(-gauge_x_pos + gauge_y_thickness, 0.0, z);
    glVertex3f(-gauge_x_pos + gauge_y_thickness, gauge_y_value, z);
    glVertex3f(-gauge_x_pos - gauge_y_thickness, gauge_y_value, z);

    glEnd();
}

#ifdef SMASHMINER_CONFIG_ENABLE_GAME_REWIND_MODE

void GameWidget::GLPaintRewindGauge()
{
    const GLfloat gauge_y_pos = 0.95 * engine->gameViewRangeY;

    const GLfloat gauge_x_range = 0.90 * engine->gameViewRangeX;

    const GLfloat tmp1 = rewindCurrentFrame - rewindFirstFrame;
    const GLfloat tmp2 = rewindLastFrame - rewindFirstFrame;

    const GLfloat gauge_x_value = gauge_x_range * 2.0 * tmp1 / tmp2;

    const GLfloat gauge_x_thickness = 0.01 * engine->gameViewRangeX;

    const GLfloat z = +75.0f;

    glColor3f(1.0f, 0.0f, 0.0f);

    glBegin(GL_LINES);

    glVertex3f(-gauge_x_range, -gauge_y_pos, z);    // x-gauge
    glVertex3f(+gauge_x_range, -gauge_y_pos, z);

    glEnd();

    glBegin(GL_QUADS);

    glVertex3f(-gauge_x_range, -gauge_y_pos - gauge_x_thickness, z);    // x-gauge
    glVertex3f(-gauge_x_range, -gauge_y_pos + gauge_x_thickness, z);
    glVertex3f(gauge_x_value - gauge_x_range, -gauge_y_pos + gauge_x_thickness, z);
    glVertex3f(gauge_x_value - gauge_x_range, -gauge_y_pos - gauge_x_thickness, z);

    glEnd();
}

#endif  // SMASHMINER_CONFIG_ENABLE_GAME_REWIND_MODE
#endif  // SMASHMINER_CONFIG_ENABLE_CONTROL_GAUGES

void GameWidget::GLDrawTriangles(GLfloat * v_data, GLfloat * n_data, int trianglecount)
{
    glBegin(GL_TRIANGLES);

    for (int i = 0;i < trianglecount;i++) {
        glNormal3fv(&n_data[i * 9 + 0]);
        glVertex3fv(&v_data[i * 9 + 0]);

        glNormal3fv(&n_data[i * 9 + 3]);
        glVertex3fv(&v_data[i * 9 + 3]);

        glNormal3fv(&n_data[i * 9 + 6]);
        glVertex3fv(&v_data[i * 9 + 6]);
    }

    glEnd();

    // draw the normals as lines.
    //^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    // startpoint is the vertex position (vector).
    // endpoint is the vertex position vector + the normal vector.

    glBegin(GL_LINES);

    glColor3f(0.0f, 1.0f, 0.0f);

    for (int i = 0;i < trianglecount;i++) {
        glVertex3fv(&v_data[i * 9 + 0]);
        glVertex3f(v_data[i * 9 + 0] + n_data[i * 9 + 0], v_data[i * 9 + 1] + n_data[i * 9 + 1], 
                   v_data[i * 9 + 2] + n_data[i * 9 + 2]);

        glVertex3fv(&v_data[i * 9 + 3]);
        glVertex3f(v_data[i * 9 + 3] + n_data[i * 9 + 3], v_data[i * 9 + 4] + n_data[i * 9 + 4], 
                   v_data[i * 9 + 5] + n_data[i * 9 + 5]);

        glVertex3fv(&v_data[i * 9 + 6]);
        glVertex3f(v_data[i * 9 + 6] + n_data[i * 9 + 6], v_data[i * 9 + 7] + n_data[i * 9 + 7], 
                   v_data[i * 9 + 8] + n_data[i * 9 + 8]);
    }

    glEnd();
}

void GameWidget::GLCleanup()
{
}

#endif  // SMASHMINER_CONFIG_USE_OPENGL_ES_2
